package com.nulldev.util.net.inetplus;

import java.net.InetAddress;
import java.util.Collection;

import com.nulldev.util.net.IPUtil;
import com.nulldev.util.net.inetplus.IInetAddress.DNSSource.DNSSourceMeta;
import com.nulldev.util.net.inetplus.IInetDNSManager.DefaultDNS;
import com.nulldev.util.other.ArrayUtils;
import com.nulldev.util.uri.URIUtil;
import com.nulldev.util.web.dns.client.Message;
import com.nulldev.util.web.dns.client.record.A;
import com.nulldev.util.web.dns.client.record.AAAA;
import com.nulldev.util.web.dns.client.record.Record;
import com.nulldev.util.web.dns.client.record.RecordData;
import com.nulldev.util.web.dns.client.transport.Resolver;

public class IInetAddress {
	
	public static class InetException extends RuntimeException {
		public InetException() {
			super();
		}
		
		public InetException(final String message) {
			super(message);
		}
		
		public InetException(final Throwable throwable) { 
			super(throwable);
		}
	}
	
	public static class DNSSource {
		private final IInetAddress source;
		private final boolean deferUDP;
		private final DNSSourceMeta meta;
		
		public DNSSource(final IInetAddress address) {
			this(address, false, DNSSourceMeta.DEFAULT);
		}
		
		public DNSSource(final IInetAddress address, final DNSSourceMeta meta) {
			this(address, false, meta);
		}
		
		public DNSSource(final IInetAddress address, final boolean deferUDP) {
			this(address, deferUDP, DNSSourceMeta.DEFAULT);
		}
		
		public DNSSource(final IInetAddress address, final boolean deferUDP, final DNSSourceMeta meta) {
			this.source = address;
			this.deferUDP = deferUDP;
			this.meta = meta;
		}
		
		public String getIP() {
			return this.source.getIP();
		}
		
		public String getIPNR() {
			return this.source.getIPNR();
		}
		
		@Override
		public boolean equals(final Object o) {
			if (o instanceof DNSSource) {
				final DNSSource comp = (DNSSource) o;
				return ((DNSSource) o).getIPNR().equals(this.getIPNR());
			}
			return (this == o);
		}

		public boolean deferUDP() {
			return this.deferUDP;
		}
		
		public DNSSourceMeta getMeta() {
			return this.meta;
		}
		
		public static class DNSSourceMeta {
			public static final DNSSourceMeta DEFAULT = new DNSSourceMeta();
			
			private String DNS_HOST_NAME;
			private boolean logging = true;
			
			public DNSSourceMeta() {
				this.DNS_HOST_NAME = "Unknown.";
			}
			
			public DNSSourceMeta(final String DNS_HOST_NAME) {
				this(DNS_HOST_NAME, true);
			}
			
			public DNSSourceMeta(final String DNS_HOST_NAME, final boolean logging) {
				this.DNS_HOST_NAME = DNS_HOST_NAME;
				this.logging = logging;
			}
			
			public String hostName() {
				return this.DNS_HOST_NAME;
			}
			
			public boolean isLogging() {
				return this.logging;
			}
			
			@Override
			public String toString() {
				return "DNSSource.Meta[hostName=" + this.DNS_HOST_NAME + ",logging=" + this.logging + "]";
			}
		}
	}

	public static enum AddressType {
		IPv4, IPv6, Unresolved;
	}
	
	private AddressType type = AddressType.Unresolved;
	
	private String ip = null; 
	private final String address;
	private final boolean shouldBeResolved;
	private DNSSourceMeta resolve_meta;
	
	public static IInetAddress createUnresolved(final String ip) throws InetException {
		if (!IPUtil.isIP(ip) && !URIUtil.isDomain(ip)) {
			throw new InetException("createUnresolved(ip) -> String given is not an IP nor URI!");
		}
		return new IInetAddress(ip, false);
	}
	
	public static IInetAddress createResolved(final String ip) throws InetException {
		if (!IPUtil.isIP(ip) && !URIUtil.isValid(ip)) {
			throw new InetException("createUnresolved(ip) -> String given is not an IP nor URI!");
		}
		return new IInetAddress(ip, true);
	}
	
	private IInetAddress(final String ip, final boolean shouldResolve) throws InetException {
		this.address = ip;
		this.type = this.getType(ip);
		this.shouldBeResolved = shouldResolve;
		if (shouldResolve) {
			this.resolveFullySkipErrors();
		}
	}
	
	private AddressType getType(final String ip) {
		if (IPUtil.isIPv6Address(ip)) {
			return AddressType.IPv6;
		}else if (IPUtil.isIPv4Address(ip)) {
			return AddressType.IPv4;
		}
		return AddressType.Unresolved;
	}

	public boolean resolved() {
		return this.ip != null;
	}
	
	void resolve() throws InetException {
		try {
			if (!IPUtil.online()) {
				throw new InetException("ERROR.NOT_CONNECTED | Not connected to a network!");
			}
			final DNSSource random = IInetDNSManager.random();
			final Resolver r = new Resolver(
					InetAddress.getByName(random.getIP()));
			if (random.deferUDP()) {
				r.setUseUdp(false);
			}
			final Message m = r.request(this.address);
			if (m == null) {
				throw new InetException("resolve() -> Message returned was null!");
			}
			for (final Record rx : m.getAnswers()) {
				final RecordData<?> s = (RecordData<?>) rx.getRecordData();
				if (s != null && s instanceof A) {
					final A a = (A) s.get();
					this.ip = a.getAddress().getHostAddress();
					this.type = getType(this.ip);
					this.resolve_meta = random.getMeta();
					break;
				}else if (s != null && s instanceof AAAA) {
					final AAAA a = (AAAA) s.get();
					this.ip = a.getAddress().getHostAddress();
					this.type = getType(this.ip);
					this.resolve_meta = random.getMeta();
					break;
				}
			}
		}catch (Throwable throwable) {
			throw new InetException(throwable);
		}
	}
	
	public String getIP() throws InetException {
		if (this.shouldBeResolved && !this.resolved()) {
			this.resolve();
		}
		if (this.ip != null) {
			return this.ip;
		}
		return this.address;
	}
	
	public String getIPNR() {
		if (this.ip != null) {
			return this.ip;
		}
		return this.address;
	}

	public void resolveFully() throws InetException {
		int attempts = 0;
		while (!this.resolved()) {
			if (attempts > 4) {
				this.legacyQuery();
				break;
			}
			attempts += 1;
			this.resolve();
		}
	}
	
	public void resolveFullySkipErrors() throws InetException {
		int attempts = 0;
		while (!this.resolved()) {
			if (attempts > 4) {
				this.legacyQuery();
				break;
			}
			attempts += 1;
			try {
				this.resolve();
			}catch (Exception e) {
				if (e != null && e.getMessage().startsWith("ERROR.NOT_CONNECTED")) {
					throw new InetException(e);
				}else {
					e.printStackTrace();
					continue;
				}
			}
		}
	}
	
	private void legacyQuery() throws InetException {
		try {
			final InetAddress inet = InetAddress.getByName(this.address);
			this.ip = inet.getHostAddress();
			this.type = getType(this.ip);
			this.resolve_meta = new DNSSourceMeta("Java Fallback DNS");
		}catch (Exception e) {
			throw new InetException(e);
		}
	}

	@Override
	public String toString() {
		return "IInetAddress[address=" + this.address + ",ip=" + this.ip + ",resolved=" + (this.ip != null) + ",type=" + this.type + 
				(this.resolve_meta != null ? ",resolveMeta=" + resolve_meta : "") + "]";
	}
	
	public AddressType getAddressType() {
		return this.type;
	}
}
